package com.atguigu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.constant.ProductConstant;
import com.atguigu.gulimall.product.entity.CategoryBrandRelationEntity;
import com.atguigu.gulimall.product.service.CategoryBrandRelationService;
import com.atguigu.gulimall.product.vo.Catelog2Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;

import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * 查询所有分类和子分类，并以树形结构数据返回
     *
     * @return
     */
    @Override
    public List<CategoryEntity> listWithTree() {
//        1、查询所有商品分类
        List<CategoryEntity> entities = baseMapper.selectList(null);

//        2、一级分类,父级id是0
        List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity ->
                categoryEntity.getParentCid() == 0 // 得到一级分类
        ).map(menu -> {
            menu.setChildren(getChildren(menu, entities)); //得到子菜单
            return menu;
        }).sorted((menu1, menu2) -> {
            // 对每级菜单排序
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList()); //得到数据转换成list集合

        return level1Menus;
    }

    /**
     * 逻辑删除
     *
     * @param asList
     */
    @Override
    public void removeMenuByIds(List<Long> asList) {
//        TODO 当前菜单是否被引用
        baseMapper.deleteBatchIds(asList);
    }

    /**
     * 通过当前的组id获得当前的所有组id
     *
     * @param catelogId
     * @return
     */
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
//        当前的属性组id获取当前类别的所有子类别的id
        List<Long> parentPath = this.findParentPath(catelogId, paths);
//        对集合进行降序排序
        Collections.reverse(parentPath);
//        转换成List的数组
        return parentPath.toArray(new Long[parentPath.size()]);
    }

    /**
     * 需求：由于品牌类别表中的名字是与品牌表，类别表中的一样，所以在更新品牌或者类别时，需要把名字更新在品牌类别关联表中。保证数据的一致性
     *
     * @param category
     * @Caching(evict = {
     * @CacheEvict(value = {"category"},key = "'getLevel1Categorys'"),
     * @CacheEvict(value = "category", key = "'getCatalogJson'")
     * }) //第一种写法：缓存失效模式
     */
    @CacheEvict(value = "category", allEntries = true) //第二种写法：缓存失效模式，所有标注的都失效
    @Transactional
    @Override
    public void updateByFild(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

    /**
     * 得到一级分类
     * 使用缓存@Cacheable，每一个需要缓存的数据我们都来指定要放到哪个名字的缓存【缓存的分区（按照业务类型分）】
     * 代表当前方法的结果需要缓存，如果缓存中有，方法都不需要调用，如果没有，则调用方法
     *
     * @return
     */
    @Cacheable(value = {"category"}, key = "#root.method.name")
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        System.out.println("getLevel1Categorys。。。。。");
        return this.baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("cat_level", 1));
    }

    //    重新实现以下
    @Cacheable(value = "category", key = "#root.methodName") //加入缓存
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        System.out.println("查询了数据库...");
//        1、查出所有数据。放到容器中，然后再筛选
        List<CategoryEntity> selectList = baseMapper.selectList(null);
//        1、查询出所有一级分类
        List<CategoryEntity> level1Catagorys = getPartenCid(selectList, 0L);
//        2、封装数据
        Map<String, List<Catelog2Vo>> parent_cid = level1Catagorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//            1、每一个一级分类，查到这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = getPartenCid(selectList, v.getCatId());
//            2、封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (null != categoryEntities) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
//                    3、找当前二级分类的三级分类，封装成vo
                    List<CategoryEntity> level3Catalog = getPartenCid(selectList, l2.getCatId());
                    if (null != level3Catalog) {
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catalog.stream().map(l3 -> {
//                            4、封装成指定格式
                            Catelog2Vo.Catelog3Vo catalog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catalog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        return parent_cid;
    }

    /**
     * 请求到缓存中得到的数据
     *
     * @return
     * @TODO 压力测试的时候报堆外内存溢出：OutOfDirectMemoryError
     * 1、springboot2.0以后默认使用lettuce作为操作redis的客户端，它使用netty进行网络通信
     * 2、lettuce的bug导致netty堆外内存溢出， -Xmx300m; netty如果没有指定堆外内存，默认使用-Xmx300m
     * 可以通过 -Dio.netty.maxDirectMemory设置
     * 解决方案：不能只修改-Dio.netty.maxDirectMemory（-Xmx300m），因为这样只会让异常出现的慢一些而已，最终还是会出现刚才异常
     * 1、升级lettuce客户端。         2、切换使用jedis（选择使用这个）
     * 使用步骤：先去掉lettuce的使用
     * <dependency>
     * <groupId>org.springframework.boot</groupId>
     * <artifactId>spring-boot-starter-data-redis</artifactId>
     * <exclusions>
     * <exclusion>
     * <groupId>io.lettuce</groupId>
     * <artifactId>lettuce-core</artifactId>
     * </exclusion>
     * </exclusions>
     * </dependency>
     * <dependency>
     * <groupId>redis.clients</groupId>
     * <artifactId>jedis</artifactId>
     * </dependency>
     */


//    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson2() {
//        1、先查看缓存中是否有数据,这里得到的数据不是json格式的，需要转换成我们需要的对象
        String catalogJSON = redisTemplate.opsForValue().get(ProductConstant.AttrEnum.REDIS_CACHE_KEY.getMsg());
        if (StringUtils.isEmpty(catalogJSON)) {
//            2、如果缓存中没有数据，先获取数据库数据，然后放入缓存中(使用分布式锁进行查询数据库,解决缓存击穿问题)
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
            /**
             * 1、加上空结果缓存：为了解决缓存穿透问题
             * 2、设置过期时间（加随机值）：解决缓存雪崩问题
             * 3、加锁：解决缓存击穿
             */
//            需要把数据转成json格式的数据才能放入缓存,好处是，json可以跨平台。
            String jsonString = JSON.toJSONString(catalogJsonFromDb);
            System.out.println("获取数据库数据");
            redisTemplate.opsForValue().set(ProductConstant.AttrEnum.REDIS_CACHE_KEY.getMsg(),
                    jsonString, 1, TimeUnit.DAYS);
            return catalogJsonFromDb;
        }
        System.out.println("缓存命中,直接返回");
//        由于我们的格式是复杂类型的，所以需要复杂类型的数据类型
        Map<String, List<Catelog2Vo>> m = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return m;
    }

    /**
     * @return
     * @TODO 如果使用redisson方式，那么就使用如下这个方法。
     * 使用redisson锁控制多线程查询数据库（每次只有一个线程进去，如果查到数据，就放入缓存）
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
//        1、锁的名字，锁的粒度越小，速度越快。
//          锁的粒度：具体缓存的是某个数据，如11号商品 product-11-lock 那么就只锁11号，如果把全部商品锁住的话，如果我们要11号与12号的商品，那么11号需要等待12号执行完才执行，
//                  但是11和12并没有关系
        RLock lock = redisson.getLock("catalogJson-lock");
        lock.lock();
//            执行业务代码(获取缓存中的数据或者数据库中的数据)
        Map<String, List<Catelog2Vo>> dataFromDb;
        try {
            dataFromDb = getDataFromDb();
        } finally {
            /**
             * 还在持有锁的状态，并且是当前线程持有的锁再解锁，不然在超高并发下会报不是同一把锁的异常
             *      IllegalMonitorStateException: attempt to unlock lock,not locked by current thread by node id:0
             */
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        return dataFromDb;
    }

    /**
     * 使用分布式锁方式
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//        1 占分布式锁,去redis占坑
        String uuid = UUID.randomUUID().toString();
//        1.1 把过期时间也设置上300s
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
//        判断锁存不存在
        if (lock) {
            System.out.println("获取分布式锁成功");
//            执行业务代码(获取缓存中的数据或者数据库中的数据)
            Map<String, List<Catelog2Vo>> dataFromDb;
            try {
                dataFromDb = getDataFromDb();
            } finally {
//            删除锁(官方写法)
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1] else return 0 end)";
                redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                        Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        } else {
            System.out.println("获取分布式锁失败,等待重试");
//        重试业务代码(自旋锁),最好等待几毫秒.
            try {
                Thread.sleep(100);
            } catch (Exception e) {

            }
            return getCatalogJsonFromDbWithRedisLock();
        }
    }

    /**
     * 查询数据库的业务代码
     *
     * @return
     */
    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
//        查询redis缓存的数据
        if (!StringUtils.isEmpty(catalogJSON)) {
//            缓存不为空直接返回
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return result;
        }
        System.out.println("查询数据库");
//        1、查出所有数据。放到容器中，然后再筛选
        List<CategoryEntity> selectList = baseMapper.selectList(null);
//        1、查询出所有一级分类
        List<CategoryEntity> level1Catagorys = getPartenCid(selectList, 0L);
//        2、封装数据
        Map<String, List<Catelog2Vo>> parent_cid = level1Catagorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//            1、每一个一级分类，查到这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = getPartenCid(selectList, v.getCatId());
//            2、封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (null != categoryEntities) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
//                    3、找当前二级分类的三级分类，封装成vo
                    List<CategoryEntity> level3Catalog = getPartenCid(selectList, l2.getCatId());
                    if (null != level3Catalog) {
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catalog.stream().map(l3 -> {
//                            4、封装成指定格式
                            Catelog2Vo.Catelog3Vo catalog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catalog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
//        查到数据库再放入缓存
        String s = JSON.toJSONString(parent_cid);
        redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
        return parent_cid;
    }


    /**
     * 查询数据库得到的数据
     *
     * @return
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithLocalLock() {
        /**
         * 优化：将多次查询数据库改为一次
         */
//        1、查出所有数据。放到容器中，然后再筛选
        List<CategoryEntity> selectList = baseMapper.selectList(null);

//        1、查询出所有一级分类
        List<CategoryEntity> level1Catagorys = getPartenCid(selectList, 0L);
//        2、封装数据
        Map<String, List<Catelog2Vo>> parent_cid = level1Catagorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//            1、每一个一级分类，查到这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = getPartenCid(selectList, v.getCatId());
//            2、封装上面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (null != categoryEntities) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
//                    3、找当前二级分类的三级分类，封装成vo
                    List<CategoryEntity> level3Catalog = getPartenCid(selectList, l2.getCatId());
                    if (null != level3Catalog) {
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catalog.stream().map(l3 -> {
//                            4、封装成指定格式
                            Catelog2Vo.Catelog3Vo catalog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catalog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        return parent_cid;
    }

    //    筛选分类方法
    private List<CategoryEntity> getPartenCid(List<CategoryEntity> selectList, Long parentCid) {
//        从集合中查找是parentCid的对象才封装到集合中
        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid() == parentCid).collect(Collectors.toList());
        return collect;
//        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", l2.parentCid));
    }


    //    通过当前类别的id获取当前类别的所有父类别的id
    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
//        先把当前类别id封装进list中
        paths.add(catelogId);
//        通过当前类别获得当前类别的信息
        CategoryEntity cateGoryId = this.getById(catelogId);
//        判断当前的父id是否为0
        if (cateGoryId.getParentCid() != 0) {
//            递归调用，一直到最外层的父节点的id为止
            findParentPath(cateGoryId.getParentCid(), paths);
        }
        return paths;
    }

    //使用递归得到每级分类下面的子分类
    private List<CategoryEntity> getChildren(CategoryEntity menu, List<CategoryEntity> entities) {
//        得到一级下的所有子菜单，并递归二级菜单的子菜单，一直递归下去
        List<CategoryEntity> collect = entities.stream().filter(categoryEntity -> {
            return categoryEntity.getParentCid() == menu.getCatId(); // 如果子节点的父id等于父节点的id
        }).map(categoryEntity -> {
            categoryEntity.setChildren(getChildren(categoryEntity, entities)); // 递归组装子节点数据
            return categoryEntity;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());

        return collect;
    }

}
